package com.nado.extensions.util;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;

/**
 * 지정된 이름의 {@link Class} 또는 {@link Object}를 생성할 때 사용되는 클래스
 *
 */
public class ReflectionUtil {

	/**
	 * 클래스 이름을 이용하여 {@link Class}를 생성한다.
	 *
	 * @param className 클래스 명
	 */
	public static Class createClass(String className) {
		if (!StringUtils.isEmpty(className)) {
			try {
				return new ResourceLoader().getClassLoader().loadClass(className);
			} catch (Exception e) {
				throw new ReflectionException(e);
			}
		}
		return null;
	}

	/**
	 * 클래스 이름을 이용하여 {@link Object}를 생성한다.
	 *
	 * @param className 클래스 명
	 */
	public static Object createInstance(String className) {
		Class clsDefault = createClass(className);
		try {
			return clsDefault.newInstance();
		} catch (Exception except) {
			throw new ReflectionException(except);
		}
	}

	/**
	 * 클래스 이름과 생성자에 전달될 인수 값을 이용하여 {@link Object}를 생성한다.
	 *
	 * @param className  클래스명
	 * @param paramValue 클래스의 생성자에 전달할 인수 값
	 * @return 생성된 객체
	 * @deprecated use {@link #createInstance(String,Class[],Object[])}
	 */
	public static Object createInstance(String className, Object... paramValue) {
		try {
			Constructor constructor = findConstructor(className, paramValue);
			return constructor.newInstance(paramValue);
		} catch (InvocationTargetException e) {
			throw new ReflectionException(e.getTargetException());
		} catch (Exception e) {
			throw new ReflectionException(e);
		}
	}

	private static Constructor findConstructor(String className, Object[] params) throws NoSuchMethodException {
		Class clazz = createClass(className);
		for (Constructor constructor : clazz.getConstructors()) {
			Class[] paramTypes = constructor.getParameterTypes();
			if (isApplicable(params, paramTypes)) {
				return constructor;
			}
		}
		throw new NoSuchMethodException(className + ".<init>(" + Arrays.asList(params) + ")");
	}

	// copy from com.nhncorp.lucy.db.ibatis.IBatisMappedHandler
	private static boolean isApplicable(Object[] params, Class[] paramTypes) {
		if (params.length != paramTypes.length) {
			return false;
		}
		for (int i = 0; i < params.length; i++) {
			if (!isApplicable(params[i].getClass(), paramTypes[i])) {
				return false;
			}
		}
		return true;
	}

	// copy from com.nhncorp.lucy.db.ibatis.IBatisMappedHandler
	public static boolean isApplicable(Class expected, Class current) {
		if (!expected.isAssignableFrom(current)) {
			if (current.isPrimitive() && expected.isAssignableFrom(ClassUtils.primitiveToWrapper(current))) {
				return true;
			}
			if (expected.isPrimitive() && ClassUtils.primitiveToWrapper(expected).isAssignableFrom(current)) {
				return true;
			}
			return false;
		}
		return true;
	}

	/**
	 * 클래스 이름과 생성자에 전달될 인수 값을 이용하여 {@link Object}를 생성한다.
	 *
	 * @param className  클래스명
	 * @param paramClazz 클래스의 생성자에 전달할 인수 값들에 대한 Type
	 * @param paramValue 클래스의 생성자에 전달할 인수 값
	 * @return 생성된 객체
	 */
	public static Object createInstance(
			String className, Class[] paramClazz, Object[] paramValue) {

		Class clsDefault = createClass(className);
		try {
			if (clsDefault != null) {
				Constructor constructor = clsDefault.getConstructor(paramClazz);
				return constructor.newInstance(paramValue);
			}
		} catch (InvocationTargetException e) {
			throw new ReflectionException(e.getTargetException());
		} catch (Exception e) {
			throw new ReflectionException(e);
		}

		return null;
	}

	/**
	 * 클래스 이름과 property map을 이용하여 Beans 객체를 생성한다.
	 * 해당 클래스는 default constructor를 제공해야 하며 그렇지 않은 경우 {@link ReflectionException}을 발생시킨다.
	 *
	 * @param className  클래스명
	 * @param properties bean 객체에 적용될 property map
	 * @return 생성된 객체
	 */
	public static Object createInstance(String className, Map properties) {
		Class clazz = createClass(className);
		try {
			Object result = clazz.newInstance();
			BeanUtils.populate(result, properties);
			return result;
		} catch (InvocationTargetException e) {
			throw new ReflectionException(e.getTargetException());
		} catch (Exception e) {
			throw new ReflectionException(e);
		}
	}

	/**
	 * 하나의 인스턴스의 field 값들을 다른 인스턴스로 복사한다.
	 * 두 인스턴스는 동일한 클래스의 인스턴스이어야 하며 그렇지 않은 경우 {@link IllegalArgumentException}을 발생시킨다.
	 *
	 * @param from field 값을 얻어올 인스턴스
	 * @param to   field 값이 복사될 인스턴스
	 */
	public static Object copyFields(Object from, Object to) throws IllegalArgumentException, IllegalAccessException {
		if (from.getClass() != to.getClass()) {
			throw new IllegalArgumentException("diffrent class type from " + from.getClass() + " to " + to.getClass());
		}
		for (Field field : from.getClass().getDeclaredFields()) {
			int modifier = field.getModifiers();
			if (!Modifier.isFinal(modifier)) {
				field.setAccessible(true);
				field.set(to, field.get(from));
			}
		}
		return to;
	}

	/**
	 * 메소드에 대해 파라미터만 포함하는 간단한 이름을 보여준다.
	 *
	 * @param method
	 * @return
	 */
	public static String getSimpleName(Method method) {
		StringBuilder builder = new StringBuilder();
		builder.append(method.getName()).append('(');
		Class<?>[] params = method.getParameterTypes();
		for (int i = 0; i < params.length; i++) {
			getTypeName(builder, params[i]);
			if (i < (params.length - 1))
				builder.append(", ");
		}
	    return builder.append(')').toString();
	}

	// from Field.getTypeName
	private static void getTypeName(StringBuilder builder, Class type) {
		if (type.isArray()) {
			try {
				Class cl = type;
				int dimensions = 0;
				while (cl.isArray()) {
					dimensions++;
					cl = cl.getComponentType();
				}
				builder.append(cl.getSimpleName());
				for (int i = 0; i < dimensions; i++) {
					builder.append("[]");
				}
				return;
			} catch (Throwable e) { /*FALLTHRU*/ }
		}
		builder.append(type.getSimpleName());
	}
}